前言
自动化打包工具webpack,相信很多人和我一样尝试着研究下它,但是繁杂的功能以及高度抽象的代码实在是很难理解,所以笔者只能通过github的webpack的第一次提交进行分析,实现,并将实现的一些心得分享一下。
功能分析
对于node端来讲,有commonjs来规范模块的标识,定义,引用。而浏览器端由于缺乏原生对象支持就需要通过自我实现来模拟commonjs规范。
webpack是通过一个IIFE立即调用函数表达式去实现这个规范的。简要的去注释,去除内部运行的代码,其格式如下:
(function(module){})([function(){},function(){}]);
简单点说就是各个模块代码以数组的形式传递给运行函数,在进行存储。详细分析可以参考简要分析webpack打包后代码
所以实现以上的功能需求点如下:
- 文件路径分析与定位resolve
- 文件编译&解析,分析出依赖文件parse
- 生成需要打包的文件树depTree
- 将依赖文件写入输出文件内writeChunk
文件分析与定位
本功能和node的require类似,故有参考node require源码
文件分析,将文件为两种类型
-
以 "./","../","/" 标识符开头的路径文件模块
- 该类文件会通过path.join 转化为真实的路径而定位。
-
自定义的文件模块
- 这类相对比较麻烦,他在当前目录下面的node_modules,查找文件,未找到则一路向上查找,最终查找到或者抛出异常。如:
[ '/Users/zhujian/Documents/workspace/webpack/simple-webpack/node_modules',
'/Users/zhujian/Documents/workspace/webpack/node_modules',
'/Users/zhujian/Documents/workspace/node_modules',
'/Users/zhujian/Documents/node_modules',
'/Users/zhujian/node_modules',
'/Users/node_modules',
'/node_modules' ]
文件定位
- 对于部分文件并没有带扩展名,此时有默认的扩展名依次以.js,.jsx为后缀依次补充。当然我们可以用传入extensions,修改默认的扩展名。
{extensions:['js','jsx','jpg']}
-
当发现该路径为文件夹时则,则依次查找如下文件
- package.json(main字段)
- index+(扩展名)
文件解析
-
文件可以定位之后,则是解析定位下来的文件了,本文用的是exprima,文档如parser规范文档
- esprima解析文件,返回一个语法树。
- 对语法树进行遍历,对遇到type 为CallExpression,且其callee为name为require的节点,将该节点的value,以及下标包装成对象储存起来。
比如
const b = require('./b');
解析后
....
"init": {
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "require",
"range": [
10,
17
]
},
"arguments": [
{
"type": "Literal",
"value": "./b",
"raw": "'./b'",
"range": [
18,
23
]
}
],
"range": [
10,
24
]
},
....
我们要做的就是提取value "./b",以及该字符串在文件所处的位置range。
文件树生成
主要是从入口文件开始,将所有依赖的js,以及其内容,分配的id组成一个可操作的扁平化的对象和存储着name与id对应的map对象。
实现手法上也是递归resolve函数,获取到各个文件的依赖,文件,id的信息,最后得到depTree对象
举个例子:
{ modules:
{ '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js':
{ filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js',
id: 0,
requires: [Array],
rangeRequires: [Array],
source: 'const b = require(\'./b\');\nconst c = require(\'c\');\nconst {e, f, g} = require(\'./m\');\n\n
},
'/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/b.js':
{ filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/b.js',
id: 1,
requires: [],
rangeRequires: [],
source: 'const b = \'b\';\n\nmodule.exports = b;\n'
},
'/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/node_modules/c.js':
{ filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/node_modules/c.js',
id: 2,
requires: [],
rangeRequires: [],
source: 'const c = \'c\';\n\nmodule.exports = c;\n'
},
'/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/m.js':
{ filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/m.js',
id: 3,
requires: [],
rangeRequires: [],
source: '// const core = require(\'./core\');\nconst a = 1;\n\n
},
nextModuleId: 4,
mapNameToId:
{ '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js': 0,
'/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/b.js': 1,
'/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/node_modules/c.js': 2,
'/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/m.js': 3
}
}
文件写入
写入主函数,替换入口的执行函数。这块会用到之前的path和id关系的map对象,通过路口文件的绝对路径,找出入口文件的mainId,并进行替换。
写入参数数组。遍历文件树,将文件节点的source内容替换掉
大致如下:
require('module') 替换为__webpack_require__(0)
-
这个地方要考虑的点是
- 如果用replace替换的话,会影响source带部分关键字的内容,不可取。
- 用索引替换的字符串的话,一旦第一个替换成功,整个字符串长度发生变化,原先的索引下标就失效了。
官方实现
const result = [source];
replaces.forEach(replace => {
const {from, value, end} = replace;
const source = result.shift();
result.unshift(source.substr(0, from), value, source.substr(end))
});
代码实现
本人的简易版webpack实现simple-webpack
(完)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。